package cz.cuni.lf1.lge.ThunderSTORM.results;

import java.awt.*;
import java.awt.image.*;
import java.awt.event.*;
import java.io.*;
import java.awt.datatransfer.*;
import ij.*;
import ij.gui.ImageCanvas;
import ij.gui.ImageWindow;
import ij.gui.NewImage;
import ij.gui.TrimmedButton;
import ij.process.*;
import ij.measure.*;
import ij.plugin.filter.Analyzer;
import ij.text.TextWindow;
import static cz.cuni.lf1.lge.ThunderSTORM.util.MathProxy.min;
import static cz.cuni.lf1.lge.ThunderSTORM.util.MathProxy.max;

/**
 * This class is an extended ImageWindow that displays histograms.
 */
public final class IJHistogramWindow extends ImageWindow implements Measurements, ActionListener, ClipboardOwner,
        MouseListener, MouseMotionListener, ImageListener, KeyListener, Runnable {

    static final int WIN_WIDTH = 300;
    static final int WIN_HEIGHT = 240;
    static final int HIST_WIDTH = 256;
    static final int HIST_HEIGHT = 128;
    static final int BAR_HEIGHT = 12;
    static final int XMARGIN = 20;
    static final int YMARGIN = 10;
    static final int INTENSITY = 0, RED = 1, GREEN = 2, BLUE = 3;
    protected ImageStatistics stats;
    protected int[] histogram;
    protected LookUpTable lut;
    protected Rectangle frame = null;
    protected Button list, save, copy, log, live, rgb, roiFilter;
    protected Label value, count;
    protected static String defaultDirectory = null;
    protected int decimalPlaces;
    protected int digits;
    protected long newMaxCount;
    protected int plotScale = 1;
    protected boolean logScale;
    protected Calibration cal;
    protected int yMax;
    public static int nBins = 256;
    private int srcImageID;			// ID of source image
    private ImagePlus srcImp;		// source image for live histograms
    private Thread bgThread;		// thread background drawing
    private boolean doUpdate;	// tells background thread to update
    private int channel;				// RGB channel
    private String blankLabel;
    private String paramName;

    /**
     * Displays a histogram using the title "Histogram of ImageName".
     */
    public IJHistogramWindow(ImagePlus imp) {
        super(NewImage.createRGBImage("Histogram of " + imp.getShortTitle(), WIN_WIDTH, WIN_HEIGHT, 1, NewImage.FILL_WHITE));
        showHistogram(imp, 256, 0.0, 0.0);
    }

    /**
     * Displays a histogram using the specified title and number of bins.
     * Currently, the number of bins must be 256 expect for 32 bit images.
     */
    public IJHistogramWindow(String title, ImagePlus imp, int bins) {
        super(NewImage.createRGBImage(title, WIN_WIDTH, WIN_HEIGHT, 1, NewImage.FILL_WHITE));
        showHistogram(imp, bins, 0.0, 0.0);
    }

    /**
     * Displays a histogram using the specified title, number of bins and
     * histogram range. Currently, the number of bins must be 256 and the
     * histogram range range must be the same as the image range expect for 32
     * bit images.
     */
    public IJHistogramWindow(String title, ImagePlus imp, int bins, double histMin, double histMax) {
        super(NewImage.createRGBImage(title, WIN_WIDTH, WIN_HEIGHT, 1, NewImage.FILL_WHITE));
        showHistogram(imp, bins, histMin, histMax);
    }

    /**
     * Displays a histogram using the specified title, number of bins, histogram
     * range and yMax.
     */
    public IJHistogramWindow(String title, ImagePlus imp, int bins, double histMin, double histMax, int yMax) {
        super(NewImage.createRGBImage(title, WIN_WIDTH, WIN_HEIGHT, 1, NewImage.FILL_WHITE));
        this.yMax = yMax;
        showHistogram(imp, bins, histMin, histMax);
    }

    /**
     * Displays a histogram using the specified title and ImageStatistics.
     */
    public IJHistogramWindow(String title, ImagePlus imp, ImageStatistics stats) {
        super(NewImage.createRGBImage(title, WIN_WIDTH, WIN_HEIGHT, 1, NewImage.FILL_WHITE));
        //IJ.log("HistogramWindow: "+stats.histMin+"  "+stats.histMax+"  "+stats.nBins);
        this.yMax = stats.histYMax;
        showHistogram(imp, stats);
    }
    
    /**
     * Displays a histogram using the specified parameter name, title and ImageStatistics.
     */
    public IJHistogramWindow(String paramName, String title, ImagePlus imp, ImageStatistics stats) {
        super(NewImage.createRGBImage(title, WIN_WIDTH, WIN_HEIGHT, 1, NewImage.FILL_WHITE));
        //IJ.log("HistogramWindow: "+stats.histMin+"  "+stats.histMax+"  "+stats.nBins);
        this.paramName = paramName;
        this.yMax = stats.histYMax;
        showHistogram(imp, stats);
    }

    /**
     * Draws the histogram using the specified title and number of bins.
     * Currently, the number of bins must be 256 expect for 32 bit images.
     */
    public void showHistogram(ImagePlus imp, int bins) {
        showHistogram(imp, bins, 0.0, 0.0);
    }

    /**
     * Draws the histogram using the specified title, number of bins and
     * histogram range. Currently, the number of bins must be 256 and the
     * histogram range range must be the same as the image range expect for 32
     * bit images.
     */
    public void showHistogram(ImagePlus imp, int bins, double histMin, double histMax) {
        boolean limitToThreshold = (Analyzer.getMeasurements() & LIMIT) != 0;
        if(channel != INTENSITY && imp.getType() == ImagePlus.COLOR_RGB) {
            ColorProcessor cp = (ColorProcessor) imp.getProcessor();
            ImageProcessor ip = cp.toFloat(channel, null);
            ImagePlus imp2 = new ImagePlus("", ip);
            imp2.setRoi(imp.getRoi());
            stats = imp2.getStatistics(AREA + MEAN + MODE + MIN_MAX, bins, histMin, histMax);
        } else {
            stats = imp.getStatistics(AREA + MEAN + MODE + MIN_MAX + (limitToThreshold ? LIMIT : 0), bins, histMin, histMax);
        }
        showHistogram(imp, stats);
    }

    /**
     * Draws the histogram using the specified title and ImageStatistics.
     */
    public void showHistogram(ImagePlus imp, ImageStatistics stats) {
        if(list == null) {
            setup(imp);
        }
        this.stats = stats;
        cal = imp.getCalibration();
        boolean limitToThreshold = (Analyzer.getMeasurements() & LIMIT) != 0;
        imp.getMask();
        histogram = stats.histogram;
        if(limitToThreshold && histogram.length == 256) {
            ImageProcessor ip = imp.getProcessor();
            if(ip.getMinThreshold() != ImageProcessor.NO_THRESHOLD) {
                int lower = scaleDown(ip, ip.getMinThreshold());
                int upper = scaleDown(ip, ip.getMaxThreshold());
                for(int i = 0; i < lower; i++) {
                    histogram[i] = 0;
                }
                for(int i = upper + 1; i < 256; i++) {
                    histogram[i] = 0;
                }
            }
        }
        lut = imp.createLut();
        int type = imp.getType();
        boolean fixedRange = type == ImagePlus.GRAY8 || type == ImagePlus.COLOR_256 || type == ImagePlus.COLOR_RGB;
        ImageProcessor ip = this.imp.getProcessor();
        ip.setColor(Color.white);
        ip.resetRoi();
        ip.fill();
        ImageProcessor srcIP = imp.getProcessor();
        drawHistogram(imp, ip, fixedRange, stats.histMin, stats.histMax);
        this.imp.updateAndDraw();
    }

    private void setup(ImagePlus imp) {
        boolean isRGB = imp.getType() == ImagePlus.COLOR_RGB;
        Panel buttons = new Panel();
        int hgap = IJ.isMacOSX() || isRGB ? 1 : 5;
        buttons.setLayout(new FlowLayout(FlowLayout.RIGHT, hgap, 0));
        int trim = IJ.isMacOSX() ? 6 : 0;
        list = new TrimmedButton("List", trim);
        list.addActionListener(this);
        buttons.add(list);
        //copy = new TrimmedButton("Copy", trim);
        //copy.addActionListener(this);
        //buttons.add(copy);
        log = new TrimmedButton("Log", trim);
        log.addActionListener(this);
        buttons.add(log);
        //live = new TrimmedButton("Live", trim);
        //live.addActionListener(this);
        //buttons.add(live);
        /*if(imp != null && isRGB) {
            rgb = new TrimmedButton("RGB", trim);
            rgb.addActionListener(this);
            buttons.add(rgb);
        }*/
        roiFilter = new TrimmedButton("Apply ROI to filter", trim);
        roiFilter.addActionListener(this);
        buttons.add(roiFilter);
        add(buttons);
        if(!(IJ.isMacOSX() && isRGB)) {
            Panel valueAndCount = new Panel();
            valueAndCount.setLayout(new GridLayout(2, 1, 0, 0));
            blankLabel = IJ.isMacOSX() ? "           " : "                ";
            value = new Label(blankLabel);
            Font font = new Font("Monospaced", Font.PLAIN, 12);
            value.setFont(font);
            valueAndCount.add(value);
            count = new Label(blankLabel);
            count.setFont(font);
            valueAndCount.add(count);
            buttons.add(valueAndCount);
        }
        pack();
    }

    public void setup() {
        setup(null);
    }

    @Override
    public void mouseMoved(int x, int y) {
        if(value == null || count == null) {
            return;
        }
        if((frame != null) && x >= frame.x && x <= (frame.x + frame.width)) {
            x = x - frame.x;
            if(x > 255) {
                x = 255;
            }
            int index = (int) (x * ((double) histogram.length) / HIST_WIDTH);
            String vlabel = null, clabel = null;
            if(blankLabel.length() == 11) // OS X
            {
                vlabel = " ";
                clabel = " ";
            } else {
                vlabel = " value=";
                clabel = " count=";
            }
            String v = vlabel + d2s(cal.getCValue(stats.histMin + index * stats.binSize)) + blankLabel;
            String c = clabel + histogram[index] + blankLabel;
            int len = vlabel.length() + blankLabel.length();
            value.setText(v.substring(0, len));
            count.setText(c.substring(0, len));
        } else {
            value.setText(blankLabel);
            count.setText(blankLabel);
        }
    }

    protected void drawHistogram(ImageProcessor ip, boolean fixedRange) {
        drawHistogram(null, ip, fixedRange, 0.0, 0.0);
    }

    void drawHistogram(ImagePlus imp, ImageProcessor ip, boolean fixedRange, double xMin, double xMax) {
        int x, y;
        long maxCount2 = 0;
        int mode2 = 0;
        int saveModalCount;

        ip.setColor(Color.black);
        ip.setLineWidth(1);
        decimalPlaces = Analyzer.getPrecision();
        digits = cal.calibrated() || stats.binSize != 1.0 ? decimalPlaces : 0;
        saveModalCount = histogram[stats.mode];
        for(int i = 0; i < histogram.length; i++) {
            if((histogram[i] > maxCount2) && (i != stats.mode)) {
                maxCount2 = histogram[i];
                mode2 = i;
            }
        }
        newMaxCount = histogram[stats.mode];
        if((newMaxCount > (maxCount2 * 2)) && (maxCount2 != 0)) {
            newMaxCount = (int) (maxCount2 * 1.5);
            //histogram[stats.mode] = newMaxCount;
        }
        if(logScale || IJ.shiftKeyDown() && !liveMode()) {
            drawLogPlot(yMax > 0 ? yMax : newMaxCount, ip);
        }
        drawPlot(yMax > 0 ? yMax : newMaxCount, ip);
        histogram[stats.mode] = saveModalCount;
        x = XMARGIN + 1;
        y = YMARGIN + HIST_HEIGHT + 2;
        if(imp == null) {
            lut.drawUnscaledColorBar(ip, x - 1, y, 256, BAR_HEIGHT);
        } else {
            drawAlignedColorBar(imp, xMin, xMax, ip, x - 1, y, 256, BAR_HEIGHT);
        }
        y += BAR_HEIGHT + 15;
        drawText(ip, x, y, fixedRange);
        srcImageID = imp.getID();
    }

    void drawAlignedColorBar(ImagePlus imp, double xMin, double xMax, ImageProcessor ip, int x, int y, int width, int height) {
        ImageProcessor ipSource = imp.getProcessor();
        float[] pixels = null;
        ImageProcessor ipRamp = null;
        if(ipSource instanceof ColorProcessor) {
            ipRamp = new FloatProcessor(width, height);
            if(channel == RED) {
                ipRamp.setColorModel(LUT.createLutFromColor(Color.red));
            } else if(channel == GREEN) {
                ipRamp.setColorModel(LUT.createLutFromColor(Color.green));
            } else if(channel == BLUE) {
                ipRamp.setColorModel(LUT.createLutFromColor(Color.blue));
            }
            pixels = (float[]) ipRamp.getPixels();
        } else {
            pixels = new float[width * height];
        }
        for(int j = 0; j < height; j++) {
            for(int i = 0; i < width; i++) {
                pixels[i + width * j] = (float) (xMin + i * (xMax - xMin) / (width - 1));
            }
        }
        if(!(ipSource instanceof ColorProcessor)) {
            ColorModel cm = null;
            if(imp.isComposite()) {
                cm = ((CompositeImage) imp).getChannelLut();
            } else if(ipSource.getMinThreshold() == ImageProcessor.NO_THRESHOLD) {
                cm = ipSource.getColorModel();
            } else {
                cm = ipSource.getCurrentColorModel();
            }
            ipRamp = new FloatProcessor(width, height, pixels, cm);
        }
        double min = ipSource.getMin();
        double max = ipSource.getMax();
        ipRamp.setMinAndMax(min, max);
        ImageProcessor bar = null;
        if(ip instanceof ColorProcessor) {
            bar = ipRamp.convertToRGB();
        } else {
            bar = ipRamp.convertToByte(true);
        }
        ip.insert(bar, x, y);
        ip.setColor(Color.black);
        ip.drawRect(x - 1, y, width + 2, height);
    }

    /**
     * Scales a threshold level to the range 0-255.
     */
    int scaleDown(ImageProcessor ip, double threshold) {
        double min = ip.getMin();
        double max = ip.getMax();
        if(max > min) {
            return (int) (((threshold - min) / (max - min)) * 255.0);
        } else {
            return 0;
        }
    }

    void drawPlot(long maxCount, ImageProcessor ip) {
        if(maxCount == 0) {
            maxCount = 1;
        }
        frame = new Rectangle(XMARGIN, YMARGIN, HIST_WIDTH, HIST_HEIGHT);
        ip.drawRect(frame.x - 1, frame.y, frame.width + 2, frame.height + 1);
        int index, y;
        for(int i = 0; i < HIST_WIDTH; i++) {
            index = (int) (i * (double) histogram.length / HIST_WIDTH);
            y = (int) (((double) HIST_HEIGHT * (double) histogram[index]) / maxCount);
            if(y > HIST_HEIGHT) {
                y = HIST_HEIGHT;
            }
            ip.drawLine(i + XMARGIN, YMARGIN + HIST_HEIGHT, i + XMARGIN, YMARGIN + HIST_HEIGHT - y);
        }
    }

    void drawLogPlot(long maxCount, ImageProcessor ip) {
        frame = new Rectangle(XMARGIN, YMARGIN, HIST_WIDTH, HIST_HEIGHT);
        ip.drawRect(frame.x - 1, frame.y, frame.width + 2, frame.height + 1);
        double max = Math.log(maxCount);
        ip.setColor(Color.gray);
        int index, y;
        for(int i = 0; i < HIST_WIDTH; i++) {
            index = (int) (i * (double) histogram.length / HIST_WIDTH);
            y = histogram[index] == 0 ? 0 : (int) (HIST_HEIGHT * Math.log(histogram[index]) / max);
            if(y > HIST_HEIGHT) {
                y = HIST_HEIGHT;
            }
            ip.drawLine(i + XMARGIN, YMARGIN + HIST_HEIGHT, i + XMARGIN, YMARGIN + HIST_HEIGHT - y);
        }
        ip.setColor(Color.black);
    }

    void drawText(ImageProcessor ip, int x, int y, boolean fixedRange) {
        ip.setFont(new Font("SansSerif", Font.PLAIN, 12));
        ip.setAntialiasedText(true);
        double hmin = cal.getCValue(stats.histMin);
        double hmax = cal.getCValue(stats.histMax);
        double range = hmax - hmin;
        if(fixedRange && !cal.calibrated() && hmin == 0 && hmax == 255) {
            range = 256;
        }
        ip.drawString(d2s(hmin), x - 4, y);
        ip.drawString(d2s(hmax), x + HIST_WIDTH - getWidth(hmax, ip) + 10, y);

        double binWidth = range / stats.nBins;
        binWidth = Math.abs(binWidth);
        boolean showBins = binWidth != 1.0 || !fixedRange;
        int col1 = XMARGIN + 5;
        int col2 = XMARGIN + HIST_WIDTH / 2;
        int row1 = y + 25;
        if(showBins) {
            row1 -= 8;
        }
        int row2 = row1 + 15;
        int row3 = row2 + 15;
        int row4 = row3 + 15;
        long count = stats.longPixelCount > 0 ? stats.longPixelCount : stats.pixelCount;
        String modeCount = " (" + stats.maxCount + ")";
        if(modeCount.length() > 12) {
            modeCount = "";
        }
        ip.drawString("Count: " + count, col1, row1);
        ip.drawString("Mean: " + d2s(stats.mean), col1, row2);
        ip.drawString("StdDev: " + d2s(stats.stdDev), col1, row3);
        ip.drawString("Mode: " + d2s(stats.dmode) + modeCount, col2, row3);
        ip.drawString("Min: " + d2s(stats.min), col2, row1);
        ip.drawString("Max: " + d2s(stats.max), col2, row2);

        if(showBins) {
            ip.drawString("Bins: " + d2s(stats.nBins), col1, row4);
            ip.drawString("Bin Width: " + d2s(binWidth), col2, row4);
        }
    }

    /*
     String d2s(double d) {
     if (d==Double.MAX_VALUE||d==-Double.MAX_VALUE)
     return "0";
     else if (Double.isNaN(d))
     return("NaN");
     else if (Double.isInfinite(d))
     return("Infinity");
     else if ((int)d==d)
     return ResultsTable.d2s(d,0);
     else
     return ResultsTable.d2s(d,decimalPlaces);
     }
     */
    private String d2s(double d) {
        if((int) d == d) {
            return IJ.d2s(d, 0);
        } else {
            return IJ.d2s(d, 8);
        }
    }

    int getWidth(double d, ImageProcessor ip) {
        return ip.getStringWidth(d2s(d));
    }

    protected void showList() {
        StringBuilder sb = new StringBuilder();
        String vheading = stats.binSize == 1.0 ? "value" : "bin start";
        if(cal.calibrated() && !cal.isSigned16Bit()) {
            for(int i = 0; i < stats.nBins; i++) {
                sb.append(i).append("\t").append(ResultsTable.d2s(cal.getCValue(stats.histMin + i * stats.binSize), digits)).append("\t").append(histogram[i]).append("\n");
            }
            TextWindow tw = new TextWindow(getTitle(), "level\t" + vheading + "\tcount", sb.toString(), 200, 400);
        } else {
            for(int i = 0; i < stats.nBins; i++) {
                sb.append(ResultsTable.d2s(cal.getCValue(stats.histMin + i * stats.binSize), digits)).append("\t").append(histogram[i]).append("\n");
            }
            TextWindow tw = new TextWindow(getTitle(), vheading + "\tcount", sb.toString(), 200, 400);
        }
    }

    protected void copyToClipboard() {
        Clipboard systemClipboard = null;
        try {
            systemClipboard = getToolkit().getSystemClipboard();
        } catch(Exception e) {
            systemClipboard = null;
        }
        if(systemClipboard == null) {
            IJ.error("Unable to copy to Clipboard.");
            return;
        }
        IJ.showStatus("Copying histogram values...");
        CharArrayWriter aw = new CharArrayWriter(stats.nBins * 4);
        PrintWriter pw = new PrintWriter(aw);
        for(int i = 0; i < stats.nBins; i++) {
            pw.print(ResultsTable.d2s(cal.getCValue(stats.histMin + i * stats.binSize), digits) + "\t" + histogram[i] + "\n");
        }
        String text = aw.toString();
        pw.close();
        StringSelection contents = new StringSelection(text);
        systemClipboard.setContents(contents, this);
        IJ.showStatus(text.length() + " characters copied to Clipboard");
    }

    void replot() {
        ImageProcessor ip = this.imp.getProcessor();
        frame = new Rectangle(XMARGIN, YMARGIN, HIST_WIDTH, HIST_HEIGHT);
        ip.setColor(Color.white);
        ip.setRoi(frame.x - 1, frame.y, frame.width + 2, frame.height);
        ip.fill();
        ip.resetRoi();
        ip.setColor(Color.black);
        if(logScale) {
            drawLogPlot(yMax > 0 ? yMax : newMaxCount, ip);
            drawPlot(yMax > 0 ? yMax : newMaxCount, ip);
        } else {
            drawPlot(yMax > 0 ? yMax : newMaxCount, ip);
        }
        this.imp.updateAndDraw();
    }

    /*
     void rescale() {
     Graphics g = img.getGraphics();
     plotScale *= 2;
     if ((newMaxCount/plotScale)<50) {
     plotScale = 1;
     frame = new Rectangle(XMARGIN, YMARGIN, HIST_WIDTH, HIST_HEIGHT);
     g.setColor(Color.white);
     g.fillRect(frame.x, frame.y, frame.width, frame.height);
     g.setColor(Color.black);
     }
     drawPlot(newMaxCount/plotScale, g);
     //ImageProcessor ip = new ColorProcessor(img);
     //this.imp.setProcessor(null, ip);
     this.imp.setImage(img);
     }
     */
    @Override
    public void actionPerformed(ActionEvent e) {
        Object b = e.getSource();
        if(b == live) {
            toggleLiveMode();
        } else if(b == rgb) {
            changeChannel();
        } else if(b == list) {
            showList();
        } else if(b == copy) {
            copyToClipboard();
        } else if(b == log) {
            logScale = !logScale;
            replot();
        } else if(b == roiFilter) {
            applyRoiAsFilter();
        }
    }

    @Override
    public void lostOwnership(Clipboard clipboard, Transferable contents) {
        //
    }

    public int[] getHistogram() {
        int[] hist = new int[histogram.length];
        for(int i = 0; i < histogram.length; i++) {
            hist[i] = (int) histogram[i];
        }
        return hist;
    }

    public double[] getXValues() {
        double[] values = new double[stats.nBins];
        for(int i = 0; i < stats.nBins; i++) {
            values[i] = cal.getCValue(stats.histMin + i * stats.binSize);
        }
        return values;
    }

    private void toggleLiveMode() {
        if(liveMode()) {
            removeListeners();
        } else {
            enableLiveMode();
        }
    }

    private void changeChannel() {
        ImagePlus improc = WindowManager.getImage(srcImageID);
        if(improc == null || improc.getType() != ImagePlus.COLOR_RGB) {
            channel = INTENSITY;
        } else {
            channel++;
            if(channel > BLUE) {
                channel = INTENSITY;
            }
            showHistogram(improc, 256);
            String name = this.imp.getTitle();
            if(name.startsWith("Red ")) {
                name = name.substring(4);
            } else if(name.startsWith("Green ")) {
                name = name.substring(6);
            } else if(name.startsWith("Blue ")) {
                name = name.substring(5);
            }
            switch(channel) {
                case INTENSITY:
                    this.imp.setTitle(name);
                    break;
                case RED:
                    this.imp.setTitle("Red " + name);
                    break;
                case GREEN:
                    this.imp.setTitle("Green " + name);
                    break;
                case BLUE:
                    this.imp.setTitle("Blue " + name);
                    break;
            }
        }
    }

    private boolean liveMode() {
        return live != null && live.getForeground() == Color.red;
    }

    private void enableLiveMode() {
        if(bgThread == null) {
            srcImp = WindowManager.getImage(srcImageID);
            if(srcImp == null) {
                return;
            }
            bgThread = new Thread(this, "Live Histogram");
            bgThread.setPriority(Math.max(bgThread.getPriority() - 3, Thread.MIN_PRIORITY));
            bgThread.start();
            imageUpdated(srcImp);
        }
        createListeners();
        if(srcImp != null) {
            imageUpdated(srcImp);
        }
    }

    // these listeners are activated if there are in the source ImagePlus
    @Override
    public synchronized void mousePressed(MouseEvent e) {
        doUpdate = true;
        notify();
    }

    @Override
    public synchronized void mouseDragged(MouseEvent e) {
        doUpdate = true;
        notify();
    }

    @Override
    public synchronized void mouseClicked(MouseEvent e) {
        doUpdate = true;
        notify();
    }

    @Override
    public synchronized void keyPressed(KeyEvent e) {
        ImagePlus improc = WindowManager.getImage(srcImageID);
        if(improc == null || improc.getRoi() != null) {
            doUpdate = true;
            notify();
        }
    }

    // unused listeners
    @Override
    public void mouseReleased(MouseEvent e) {
        //
    }

    @Override
    public void mouseExited(MouseEvent e) {
        //
    }

    @Override
    public void mouseEntered(MouseEvent e) {
        //
    }

    @Override
    public void mouseMoved(MouseEvent e) {
        //
    }

    @Override
    public void imageOpened(ImagePlus imp) {
        //
    }

    @Override
    public void keyTyped(KeyEvent e) {
        //
    }

    @Override
    public void keyReleased(KeyEvent e) {
        //
    }

    // This listener is called if the source image content is changed
    @Override
    public synchronized void imageUpdated(ImagePlus imp) {
        if(imp == srcImp) {
            doUpdate = true;
            notify();
        }
    }

    // If either the source image or this image are closed, exit
    @Override
    public void imageClosed(ImagePlus imp) {
        if(imp == srcImp || imp == this.imp) {
            if(bgThread != null) {
                bgThread.interrupt();
            }
            bgThread = null;
            removeListeners();
            srcImp = null;
        }
    }

    // the background thread for live plotting.
    @Override
    public void run() {
        while(true) {
            if(doUpdate && srcImp != null) {
                if(srcImp.getRoi() != null) {
                    IJ.wait(50);	//delay to make sure the roi has been updated
                }
                if(srcImp != null) {
                    if(srcImp.getBitDepth() == 16 && ImagePlus.getDefault16bitRange() != 0) {
                        showHistogram(srcImp, 256, 0, Math.pow(2, ImagePlus.getDefault16bitRange()) - 1);
                    } else {
                        showHistogram(srcImp, 256);
                    }
                }
            }
            synchronized(this) {
                if(doUpdate) {
                    doUpdate = false;		//and loop again
                } else {
                    try {
                        wait();
                    } //notify wakes up the thread
                    catch(InterruptedException e) { //interrupted tells the thread to exit
                        return;
                    }
                }
            }
        }
    }

    private void createListeners() {
        //IJ.log("createListeners");
        if(srcImp == null) {
            return;
        }
        ImageCanvas canvas = srcImp.getCanvas();
        if(canvas == null) {
            return;
        }
        canvas.addMouseListener(this);
        canvas.addMouseMotionListener(this);
        canvas.addKeyListener(this);
        ImagePlus.addImageListener(this);
        Font font = live.getFont();
        live.setFont(new Font(font.getName(), Font.BOLD, font.getSize()));
        live.setForeground(Color.red);
    }

    private void removeListeners() {
        //IJ.log("removeListeners");
        if(srcImp == null) {
            return;
        }
        ImageCanvas canvas = srcImp.getCanvas();
        canvas.removeMouseListener(this);
        canvas.removeMouseMotionListener(this);
        canvas.removeKeyListener(this);
        ImagePlus.removeImageListener(this);
        Font font = live.getFont();
        live.setFont(new Font(font.getName(), Font.PLAIN, font.getSize()));
        live.setForeground(Color.black);
    }

    private void applyRoiAsFilter() {
        Rectangle roi = this.imp.getProcessor().getRoi();
        int left = max(frame.x, roi.x) - frame.x;
        int right = min((frame.x + frame.width - 1), (roi.x + roi.width - 1)) - frame.x;
        
        int indexLeft  = (int) (left  * ((double) histogram.length) / HIST_WIDTH);
        int indexRight = (int) (right * ((double) histogram.length) / HIST_WIDTH);
            
        double leftVal  = cal.getCValue(stats.histMin + indexLeft  * stats.binSize);    // param > val
        double rightVal = cal.getCValue(stats.histMin + indexRight * stats.binSize);    // param < val
        
        IJResultsTable.getResultsTable().tableWindow.getFilter().addNewFilter(paramName, leftVal, rightVal);
    }
}
